What is Amazon Lex?
Build production-grade conversational AI without ML expertise
Amazon Lex is AWS's fully managed, low-code service for building conversational interfaces — chatbots and voice bots — using the same deep learning technology that powers Alexa. You define the conversation logic visually or via JSON/API, and AWS handles all the NLP, ASR (speech recognition), and infrastructure.
Where Lex Sits in the AWS AI Stack
Lex vs Alternatives — When to Pick Lex
| Criteria | Amazon Lex | Dialogflow CX | Copilot Studio |
|---|---|---|---|
| Ecosystem | AWS-native | Google Cloud | Microsoft 365 |
| LLM Integration | Bedrock + Claude | Vertex AI | Azure OpenAI |
| Voice (ASR) | Built-in Polly/Transcribe | Dialogflow STT | Azure Speech |
| Low-Code UI | Visual flow builder | Page/flow designer | Canvas designer |
| Best for | AWS workloads, Contact Center | GCP workloads | M365 enterprise |
Amazon Lex V2 (current version) introduced a visual conversation flow designer and multi-bot aliasing — the older V1 API is deprecated for new projects. Always use V2.
Core Concepts
The vocabulary you must know to build anything in Lex
🤖 Bot
Top-level container. Holds all intents, slot types, and configurations. Has a name, language setting, and IAM role.
🎯 Intent
Represents what a user wants to do. E.g., OrderPizza, CheckBalance, BookFlight.
💬 Utterance
Sample phrases users might say to trigger an intent. "I want a pizza", "Order me a large margherita".
📦 Slot
A piece of data Lex needs to collect. Like a form field. E.g., PizzaSize, ToppingType.
🏷️ Slot Type
Defines valid values for a slot. Built-in types (dates, numbers) or custom enumerations you define.
⚙️ Fulfillment
What happens after all slots are filled. Either a Lambda hook or a simple static response message.
The Conversation Lifecycle
Confidence Score & Fallback
Every intent match has a confidence score (0–1). If no intent passes the threshold, Lex triggers the built-in FallbackIntent. You can configure what happens there — route to an agent, ask for clarification, or connect to Amazon Bedrock for LLM-generated responses.
Think of Lex as a router + form filler. It's excellent at extracting structured data from natural language. For open-ended chat or reasoning, chain it with Bedrock/Claude via Lambda.
Bot Versioning & Aliases
| Concept | Purpose | Example |
|---|---|---|
| Draft | Working version, not live | Your dev sandbox |
| Version | Immutable snapshot | Version 1, 2, 3... |
| Alias | Named pointer to a version | PROD → Version 3 |
Aliases let you do blue/green deployments: create a new version, test it under a staging alias, then switch the PROD alias with zero downtime.
🧠 Quick Check
Console Walkthrough
Navigating the Amazon Lex V2 console from top to bottom
AWS account with IAM user/role. Lex requires an IAM role with AmazonLexFullAccess + AWSLambdaRole if using Lambda fulfillment.
Console Sections at a Glance
| Section | What you do there |
|---|---|
| Bots | List, create, import/export bots |
| Bot > Intents | Create intents, add utterances, configure slots |
| Bot > Slot types | Define custom enumerated types with synonyms |
| Bot > Visual flow | Drag-and-drop conversation graph editor |
| Bot > Versions | Create immutable snapshots |
| Bot > Aliases | Map aliases to versions, configure Lambda |
| Bot > Channel integrations | Connect to Slack, Facebook Messenger, etc. |
| Test console (inline) | Chat in-browser to test NLU + dialogue |
Step-by-Step: Creating a Bot
Go to console.aws.amazon.com/lex → Select region (e.g. us-east-1) → Click Create bot.
Select "Create a blank bot" for full control, or use a pre-built template (BookTrip, OrderFlowers). Templates are great for learning.
Give your bot a unique name. For IAM role: choose "Create a role with basic Amazon Lex permissions" for auto-setup.
COPPA: No (for general apps). Idle session TTL: 5 minutes is standard. This controls how long sessions are remembered.
English (US) is most feature-complete. Lex supports 10+ languages. You can add multiple languages to one bot.
You'll land in the Intents list. Two intents are auto-created: FallbackIntent and you create the rest.
Your First Bot
A complete Pizza Ordering bot — from zero to working chatbot
We'll build PizzaBot — a bot that takes pizza orders. It will collect size, crust, and topping, then confirm the order. This teaches every core concept hands-on.
Bot Architecture Plan
Step 1: Define the Intent
In the Lex console under your bot → Intents → Add intent → Name it OrderPizza.
Step 2: Add Utterances
Add at least 10 varied utterances. Lex learns from them to match user input:
# Sample utterances for OrderPizza intent
I want to order a pizza
Can I get a pizza please
Order me a {PizzaSize} pizza
I'd like a {PizzaSize} {CrustType} crust pizza
Get me a pizza with {Toppings}
I want a large pizza with extra cheese
Place a pizza order for me
Can you order a medium thin crust pizza
I'd like to place a pizza order
One pizza pleaseNotice {PizzaSize} in utterances — Lex will extract the slot value directly from the phrase. This speeds up the conversation by skipping the "What size?" prompt.
Step 3: Create Slot Types
Before creating slots, define the custom types. Go to Slot types → Add slot type:
# Slot Type: PizzaSizeType Type: Custom slot type (Expand values) Values: - Small (synonyms: sm, personal) - Medium (synonyms: med, regular) - Large (synonyms: lg, big, family) - Extra Large (synonyms: XL, extra-large) # Slot Type: CrustType Values: - Thin (synonyms: thin-crust, crispy) - Thick (synonyms: deep-dish, pan) - Stuffed (synonyms: cheese-stuffed) - Gluten-free # Slot Type: ToppingType (AMAZON.AlphaNumeric also works) Values: - Cheese, Pepperoni, Mushrooms, Onions - Olives, Peppers, Bacon, Pineapple
Step 4: Add Slots to the Intent
Back in your OrderPizza intent → Slots section → Add three slots:
| Slot Name | Slot Type | Prompt (Lex asks this) | Required? |
|---|---|---|---|
PizzaSize | PizzaSizeType | "What size pizza would you like?" | Yes |
CrustType | CrustType | "What type of crust would you like?" | Yes |
Toppings | ToppingType | "What toppings would you like?" | No |
Step 5: Confirmation Prompt
Enable the Confirmation prompt in the intent settings:
Confirmation prompt: "Just to confirm: a {PizzaSize} pizza with {CrustType} crust" "and {Toppings}. Shall I place this order?" Decline response: "No problem, order cancelled. Let me know if you'd like anything else."
Step 6: Build & Test
Click Build (top right). Wait ~30 seconds. Then use the Test panel to have a conversation:
You: I want a pizza Bot: What size pizza would you like? You: Large Bot: What type of crust would you like? You: Thin crust Bot: What toppings would you like? You: Pepperoni and mushrooms Bot: Just to confirm: a Large pizza with Thin crust and Pepperoni and mushrooms. Shall I place this order? You: Yes Bot: [Fulfillment response]
Intents & Utterances
Designing intents that understand natural human language
Intent Lifecycle States
| State | Meaning | Your action |
|---|---|---|
InProgress | Collecting slot values | Lex prompts user for each slot |
ReadyForFulfillment | All required slots filled | Trigger Lambda or return response |
Fulfilled | Lambda/response returned | Show result to user |
Failed | Lambda threw an error | Show error message |
Built-in Intents
AMAZON.FallbackIntent
- Triggered when no intent matches
- Use to transfer to agent or trigger LLM
- Always exists, can't be deleted
AMAZON.KendraSearchIntent
- Auto-queries Amazon Kendra
- Returns FAQ-style answers
- Ideal for knowledge base bots
Utterance Best Practices
Don't add 3 utterances and wonder why NLU fails. Lex needs 15–30 diverse utterances per intent for robust recognition. Vary structure, not just wording.
# ❌ BAD - Too similar, low diversity I want to book a flight I would like to book a flight Book me a flight # ✅ GOOD - Structural variety I want to book a flight Can I get a ticket to {Destination} Book me a {CabinClass} seat on {Date} What flights are available to {Destination} I need to fly to {Destination} on {Date} Get me on a flight to {Destination} Reserve a seat for me I'd like to make a flight reservation Schedule a flight for next {Date} {Destination} please, flying {Date}
Intent Priority & Disambiguation
When multiple intents could match, Lex picks the highest confidence score. If two intents score similarly, you can configure clarification prompts — Lex asks "Did you mean X or Y?"
# Enable in console: Bot Settings > Clarification Clarification prompt: "I'm not sure what you mean. Did you want to (1) check order status or (2) place a new order?" Max clarification retries: 2 If still unclear: trigger FallbackIntent
Closing Responses
Every intent should have a closing response for when it ends successfully. This replaces the plain "Intent fulfilled" default message.
# In Lex Console: Intent > Closing response Message: "Your {PizzaSize} pizza has been ordered! ETA: 30 minutes. Enjoy! 🍕" # You can also use session attributes in messages: Message: "Order #{orderRef} confirmed for {CustomerName}. Check your email for details."
Slots & Slot Types
Data collection at the heart of every Lex conversation
Built-in Slot Types (AMAZON.*)
| Slot Type | Captures | Example |
|---|---|---|
AMAZON.Date | Dates in any format | "next Monday" → 2025-01-20 |
AMAZON.Time | Times | "3pm" → 15:00 |
AMAZON.Duration | Time spans | "2 hours" → PT2H |
AMAZON.Number | Integers/floats | "forty two" → 42 |
AMAZON.EmailAddress | Email strings | [email protected] |
AMAZON.PhoneNumber | Phone numbers | +1-800-555-0100 |
AMAZON.City | US city names | New York, Boston |
AMAZON.AlphaNumeric | Any word/phrase | Free-form string |
Custom Slot Types
Two modes for custom types:
Expand Values
Lex recognizes your values plus similar words via NLU. Best for open-ended categories.
Values: ["pizza", "burger", "sushi"] → Also matches "pie", "sandwich"
Restrict to Slot Values
Lex only accepts exact matches + synonyms. Use for controlled data like product codes.
Values: ["PLAN_A", "PLAN_B"] → Rejects anything else
Slot Prompts & Retries
# Slot configuration in Lex console Slot: DepartureCity Type: AMAZON.City Required: true Prompts: "Which city are you flying from?" "What's your departure city?" # Retry 1 "Please tell me where you're departing from." # Retry 2 Max retries: 2 If still not filled: Close with failure response
Slot Validation via Lambda
Use a validation hook to check slot values in real-time before the conversation proceeds:
import json def lambda_handler(event, context): invocation_source = event['invocationSource'] if invocation_source == 'DialogCodeHook': # Validate slots on every turn slots = event['sessionState']['intent']['slots'] pizza_size = slots.get('PizzaSize') if pizza_size and pizza_size['value']['interpretedValue'] not in ['Small','Medium','Large']: # Invalid value — re-elicit the slot return elicit_slot(event, 'PizzaSize', "Sorry, we only have Small, Medium, or Large. Which would you like?") # All good — delegate back to Lex return delegate(event) elif invocation_source == 'FulfillmentCodeHook': return fulfill_order(event) def elicit_slot(event, slot_name, message): return { 'sessionState': { 'dialogAction': {'type': 'ElicitSlot', 'slotToElicit': slot_name}, 'intent': event['sessionState']['intent'] }, 'messages': [{'contentType': 'PlainText', 'content': message}] } def delegate(event): return { 'sessionState': { 'dialogAction': {'type': 'Delegate'}, 'intent': event['sessionState']['intent'] } }
Fulfillment & Lambda
Connecting your bot to real backend systems
Two Fulfillment Modes
Static Response
Return a hardcoded message with slot values interpolated. No Lambda needed. Use for simple confirmations.
"Thanks {CustomerName}! Your order is placed."
Lambda Hook
Call a Lambda function with the full session state. Run business logic, query databases, call APIs.
Lambda → DynamoDB / Stripe / CRM
The Lambda Event Structure
Lex sends this JSON to your Lambda function. Understanding it is critical:
{
"sessionId": "user-abc-123",
"invocationSource": "FulfillmentCodeHook", // or "DialogCodeHook"
"inputTranscript": "Large pepperoni thin crust please",
"sessionState": {
"intent": {
"name": "OrderPizza",
"state": "ReadyForFulfillment",
"slots": {
"PizzaSize": {
"value": {
"originalValue": "large",
"interpretedValue": "Large",
"resolvedValues": ["Large"]
}
},
"CrustType": {
"value": {"interpretedValue": "Thin"}
},
"Toppings": {
"value": {"interpretedValue": "Pepperoni"}
}
}
},
"sessionAttributes": {
"userId": "user-abc-123"
}
}
}Complete Fulfillment Lambda (Python)
import json import boto3 from datetime import datetime import uuid dynamodb = boto3.resource('dynamodb') orders_table = dynamodb.Table('PizzaOrders') def lambda_handler(event, context): source = event['invocationSource'] if source == 'FulfillmentCodeHook': slots = event['sessionState']['intent']['slots'] # Extract slot values safely def get_slot(name): s = slots.get(name) return s['value']['interpretedValue'] if s else None size = get_slot('PizzaSize') crust = get_slot('CrustType') toppings = get_slot('Toppings') or 'Cheese only' # Generate order ID and save to DynamoDB order_id = str(uuid.uuid4())[:8].upper() orders_table.put_item(Item={ 'orderId': order_id, 'size': size, 'crust': crust, 'toppings': toppings, 'timestamp': datetime.utcnow().isoformat(), 'status': 'confirmed' }) message = f"Order #{order_id} confirmed! " \ f"Your {size} {crust} crust pizza with {toppings} " \ "will be ready in ~25 minutes." return close_intent(event, message, 'Fulfilled') return close_intent(event, "Something went wrong. Please try again.", 'Failed') def close_intent(event, message, state): intent = event['sessionState']['intent'] intent['state'] = state return { 'sessionState': { 'dialogAction': {'type': 'Close'}, 'intent': intent, 'sessionAttributes': event['sessionState'].get('sessionAttributes', {}) }, 'messages': [{'contentType': 'PlainText', 'content': message}] }
You must add a resource-based policy to Lambda allowing Lex to invoke it. In Lex console under Alias → Lambda → select your function. AWS adds the permission automatically.
Conversation Flow
Visual flow designer, session state, and multi-intent conversations
Visual Flow Designer
Lex V2's Visual Conversation Builder lets you design branching dialogue without code. Access it via Bot → Visual conversation builder.
Session Attributes
Session attributes are key-value pairs that persist across the entire session (all intents). Use them to carry context:
# Lambda: Set a session attribute session_attrs = event['sessionState'].get('sessionAttributes', {}) session_attrs['customerTier'] = 'GOLD' session_attrs['cartTotal'] = '29.99' # Return with updated session attributes return { 'sessionState': { 'dialogAction': {'type': 'Delegate'}, 'intent': event['sessionState']['intent'], 'sessionAttributes': session_attrs # ← persisted! } }
Request Attributes vs Session Attributes
| Type | Scope | Use Case |
|---|---|---|
| Session Attributes | Entire session (all turns) | User ID, cart, preferences |
| Request Attributes | Current turn only | UI context, page ID, A/B test variant |
Switching Intents Mid-Conversation
Users often switch topics. Lex handles this via intent chaining. You can force a switch via Lambda:
# Lambda: switch to a different intent return { 'sessionState': { 'dialogAction': {'type': 'ElicitIntent'}, 'intent': {'name': 'CheckOrderStatus'}, # jump here 'sessionAttributes': session_attrs }, 'messages': [{ 'contentType': 'PlainText', 'content': 'Sure! Let me check your order status. What\\'s your order number?' }] }
Dialog Actions Reference
| Action | Effect |
|---|---|
Delegate | Hand control back to Lex to continue dialogue |
ElicitSlot | Ask user for a specific slot value |
ElicitIntent | Ask user what they want to do |
ConfirmIntent | Ask yes/no confirmation |
Close | End the intent (fulfilled or failed) |
Multi-Channel Deployment
Publishing your bot to web, Slack, Facebook, and Amazon Connect
Deployment Architecture
Option A: Embedded Web Chat (AWS Amplify)
// Install: npm install @aws-sdk/client-lex-runtime-v2 import { LexRuntimeV2Client, RecognizeTextCommand } from "@aws-sdk/client-lex-runtime-v2"; const client = new LexRuntimeV2Client({ region: "us-east-1", credentials: cognitoCredentials // from Cognito Identity Pool }); async function sendMessage(text) { const command = new RecognizeTextCommand({ botId: "YOUR_BOT_ID", botAliasId: "TSTALIASID", // or your alias localeId: "en_US", sessionId: "user-session-123", text: text }); const response = await client.send(command); const reply = response.messages?.[0]?.content; console.log("Bot says:", reply); return reply; } // Usage sendMessage("I want to order a large pizza");
Option B: Slack Integration
Go to api.slack.com/apps → Create New App → Enable Bot Token Scopes: chat:write, im:history.
Bot → Aliases → PROD → Add channel integration → Slack → Enter Bot Token + Signing Secret.
Copy the callback URL from Lex and paste into Slack App Event Subscriptions. Verify challenge.
Option C: Amazon Connect (Voice Bot)
This is the most powerful deployment — a full IVR / voice contact center with Lex as the NLU brain:
# Amazon Connect Contact Flow snippet (JSON) { "Type": "GetParticipantInput", "Parameters": { "Text": "Welcome to PizzaBot! How can I help you today?", "LexV2Bot": { "AliasArn": "arn:aws:lex:us-east-1:123456789:bot-alias/BOTID/ALIASID" } } }
Using API Gateway for Custom Web Integration
# Lambda proxy to Lex (keeps credentials server-side) import boto3, json lex = boto3.client('lexv2-runtime', region_name='us-east-1') def lambda_handler(event, context): body = json.loads(event['body']) response = lex.recognize_text( botId='YOUR_BOT_ID', botAliasId='YOUR_ALIAS_ID', localeId='en_US', sessionId=body['sessionId'], text=body['message'] ) reply = response['messages'][0]['content'] if response.get('messages') else '' return { 'statusCode': 200, 'headers': {'Access-Control-Allow-Origin': '*'}, 'body': json.dumps({'reply': reply}) }
Analytics & Testing
Measuring bot performance and catching failures before production
Built-in Analytics Dashboard
Lex console → Analytics shows:
📊 Conversation Metrics
- Total conversations
- Missed utterances
- Intent success rate
- Drop-off points
🎯 Intent Metrics
- Per-intent recognition rate
- Slot fill rate
- Fulfillment success/failure
- Avg. turns to complete
Missed Utterances — Your #1 Improvement Tool
The Missed Utterances report shows exactly what users said that Lex couldn't match. Review weekly and add them as utterances:
# Example missed utterances to review: "gimme a pizza" → Add to OrderPizza "what's in my cart" → Add to CheckCart intent "cancel that" → Add to CancelOrder "speak to a person" → Add to EscalateToAgent
Testing with the CLI (Automated)
# Test a Lex bot via AWS CLI aws lexv2-runtime recognize-text \ --bot-id YOUR_BOT_ID \ --bot-alias-id TSTALIASID \ --locale-id en_US \ --session-id test-session-001 \ --text "I want a large pepperoni pizza" # Response shows intent + slots: { "sessionId": "test-session-001", "messages": [{"content": "What type of crust would you like?"}], "sessionState": { "intent": { "name": "OrderPizza", "state": "InProgress", "slots": { "PizzaSize": {"value": {"interpretedValue": "Large"}}, "Toppings": {"value": {"interpretedValue": "Pepperoni"}}, "CrustType": null # still being elicited } } } }
Automated Test Script (Python)
import boto3, json lex = boto3.client('lexv2-runtime') test_cases = [ {"input": "I want a pizza", "expected_intent": "OrderPizza"}, {"input": "What's my order status", "expected_intent": "CheckOrderStatus"}, {"input": "gibberish xyz 123", "expected_intent": "FallbackIntent"}, ] for i, test in enumerate(test_cases): resp = lex.recognize_text( botId='YOUR_BOT_ID', botAliasId='TSTALIASID', localeId='en_US', sessionId=f'test-{i}', text=test['input'] ) actual = resp['sessionState']['intent']['name'] status = '✅ PASS' if actual == test['expected_intent'] else '❌ FAIL' print(f"{status} | '{test['input']}' → {actual}")
🧠 Quick Check
Real-World Project
Build a complete Banking Support Bot — BankBot
This capstone project builds BankBot — a production-grade banking assistant that handles balance inquiries, transfers, and fraud alerts. It integrates everything you've learned.
BankBot Intent Map
| Intent | Utterances (sample) | Slots | Fulfillment |
|---|---|---|---|
| CheckBalance | "What's my balance", "How much do I have" | AccountType | Lambda → DynamoDB |
| TransferFunds | "Transfer money", "Send $50 to savings" | Amount, FromAccount, ToAccount | Lambda → Transfer API |
| ReportFraud | "Report fraud", "My card was stolen" | CardLast4, IncidentDate | Lambda → SNS alert |
| EscalateAgent | "Talk to a human", "Agent please" | — | Lambda → Connect queue |
Full Lambda Architecture
import boto3, json dynamodb = boto3.resource('dynamodb') sns = boto3.client('sns') accounts = dynamodb.Table('BankAccounts') def lambda_handler(event, context): intent_name = event['sessionState']['intent']['name'] source = event['invocationSource'] slots = event['sessionState']['intent'].get('slots', {}) session = event['sessionState'].get('sessionAttributes', {}) def get_slot(name): s = slots.get(name) return s['value']['interpretedValue'] if s else None if intent_name == 'CheckBalance' and source == 'FulfillmentCodeHook': user_id = session.get('userId', 'demo-user') acct_type = get_slot('AccountType') or 'checking' # Look up balance result = accounts.get_item(Key={'userId': user_id, 'accountType': acct_type}) balance = result.get('Item', {}).get('balance', 'N/A') return close(event, f"Your {acct_type} balance is ${balance:,.2f}.") elif intent_name == 'ReportFraud' and source == 'FulfillmentCodeHook': card = get_slot('CardLast4') # Send SNS alert to fraud team sns.publish( TopicArn='arn:aws:sns:us-east-1:123456789:FraudAlerts', Subject=f'Fraud Report - Card ending {card}', Message=json.dumps({'userId': session.get('userId'), 'card': card}) ) return close(event, f"Fraud report filed for card ending {card}. " "Your card has been blocked. A specialist will contact you within 2 hours.") elif intent_name == 'TransferFunds' and source == 'FulfillmentCodeHook': amount = get_slot('Amount') from_acct = get_slot('FromAccount') to_acct = get_slot('ToAccount') # Real transfer logic goes here return close(event, f"${amount} transferred from {from_acct} to {to_acct}. Done! ✅") return close(event, "I'm not sure how to help with that. Type 'agent' to speak with someone.") def close(event, message): intent = event['sessionState']['intent'] intent['state'] = 'Fulfilled' return { 'sessionState': { 'dialogAction': {'type': 'Close'}, 'intent': intent, 'sessionAttributes': event['sessionState'].get('sessionAttributes', {}) }, 'messages': [{'contentType': 'PlainText', 'content': message}] }
Security Checklist for Production
- ✅ Authentication: Pass user ID via session attributes from your auth layer (Cognito/JWT)
- ✅ No PII in Lex logs: Disable conversation logs or use CloudWatch log filtering
- ✅ IAM least-privilege: Lambda role should only access the specific DynamoDB tables it needs
- ✅ Input validation: Always validate slot values in Lambda, never trust Lex output blindly
- ✅ Rate limiting: Use API Gateway throttling on any public-facing Lex proxy endpoint
- ✅ Bot versioning: Always deploy via aliases, never point directly to DRAFT in production
In your FallbackIntent Lambda, call Amazon Bedrock/Claude to handle questions Lex can't match. This creates a hybrid bot: structured tasks go through Lex intents, open-ended questions go to Claude. This is the modern agentic chatbot architecture.
You now know: Lex core concepts, intent/slot/utterance design, Lambda fulfillment, conversation flow control, multi-channel deployment, analytics, and a real production architecture. Next step: build your own bot and integrate with Bedrock for LLM fallback.